#include "mapsmanager.h"
#include "configuration.h"
#include "map.h"
#include "mapcalibrator.h"

#include "controls/ftlabel.h"
#include "controls/ftcheckbox.h"
#include "controls/ftwidgetlist.h"
#include "controls/ftpushbutton.h"
#include "controls/ftpushbuttonicon.h"
#include "controls/ftmessagebox.h"
#include "controls/ftvboxlayout.h"
#include "controls/fthboxlayout.h"
#include "controls/filebrowser.h"

MapsManager::MapsManager() :
    RotatingWidget()
{    
    // TODO: instead of hardcoding this, try to calculate it...
    QPixmapCache::setCacheLimit( 20000 );
    selectedMap = NULL;
    buildMainPage();
    buildEditPage();

    QString path = configuration->defaultPath() + "/map_storage";
    if ( !QFile::exists( path ) )
        QDir().mkpath( path );

    configFile.setFileName( path + "/maps.cfg");
    if ( configFile.exists() )
    {
        if ( configFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
        {
            QTextStream stream( &configFile );
            while ( !stream.atEnd() )
            {
                QString map_data = stream.readLine();
                if ( map_data.length() > 1 )
                {
                    QTextStream map_stream( &map_data, QIODevice::ReadOnly );
                    Map* map = new Map( map_stream );
                    if ( map->isValidMap() )
                        appendMap( map );
                    else
                        delete map;
                }
            }
            configFile.close();
        }
    }
}

void MapsManager::saveConfigFile()
{
    if ( configFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
    {
        QTextStream stream( &configFile );
        foreach ( Map* map, maps )
        {
            QString str = map->serializeMap();
            stream << str << "\n";
        }
        configFile.close();
    }
}

QLayout* MapsManager::createVertical()
{
    FTVBoxLayout* grid;
    FTHBoxLayout* hgrid;

    if ( editPage->layout() != NULL )
        delete editPage->layout();
    grid = new FTVBoxLayout();
    grid->addWidget( titleLabel );
    grid->addWidget( titleEdit );
    grid->addWidget( enableMapButton );
    grid->addWidget( calibratedLabel );
    grid->addStretch();
    grid->addWidget( deleteMapButton );
    grid->addWidget( loadCalibrationButton );
    grid->addWidget( calibrationButton );
    hgrid = new FTHBoxLayout();
    hgrid->addWidget( saveButton );
    hgrid->addWidget( cancelButton );
    grid->addLayout( hgrid );
    editPage->setLayout( grid );

    if ( mainPage->layout() != NULL )
        delete mainPage->layout();
    grid = new FTVBoxLayout();
    grid->addWidget( mapsList );
    hgrid = new FTHBoxLayout();
    hgrid->addWidget( loadButton );
    hgrid->addWidget( showButton );
    grid->addLayout( hgrid );
    grid->addWidget( numMaps );
    mainPage->setLayout( grid );

    grid = new FTVBoxLayout();
    grid->addWidget( pages );
    return grid;
}

QLayout* MapsManager::createHorizontal()
{
    FTVBoxLayout* grid;
    FTHBoxLayout* hgrid;

    if ( editPage->layout() != NULL )
        delete editPage->layout();
    grid = new FTVBoxLayout();
    grid->addWidget( titleLabel );
    grid->addWidget( titleEdit );
    hgrid = new FTHBoxLayout();
    hgrid->addWidget( enableMapButton );
    hgrid->addWidget( calibratedLabel );
    grid->addLayout( hgrid );
    grid->addStretch();
    hgrid = new FTHBoxLayout();
    hgrid->addWidget( loadCalibrationButton );
    hgrid->addWidget( calibrationButton );
    hgrid->addWidget( deleteMapButton );
    grid->addLayout( hgrid );
    hgrid = new FTHBoxLayout();
    hgrid->addWidget( saveButton );
    hgrid->addWidget( cancelButton );
    grid->addLayout( hgrid );
    editPage->setLayout( grid );

    if ( mainPage->layout() != NULL )
        delete mainPage->layout();
    grid = new FTVBoxLayout();
    grid->addWidget( mapsList );
    hgrid = new FTHBoxLayout();
    hgrid->addWidget( loadButton );
    hgrid->addWidget( showButton );
    grid->addLayout( hgrid );
    grid->addWidget( numMaps );
    mainPage->setLayout( grid );

    grid = new FTVBoxLayout();
    grid->addWidget( pages );
    return grid;
}

void MapsManager::invalidateBlocks()
{
    QPixmapCache::clear();
}

void MapsManager::buildEditPage()
{
    editTabs = new QStackedWidget();
    pages->addWidget( editTabs );

    editPage = new QWidget();
    editTabs->addWidget( editPage );

    titleLabel = new FTLabel( tr("Map title:") );
    titleEdit = new QLineEdit();

    enableMapButton = new FTCheckBox( tr("Map enabled") );
    deleteMapButton = new FTPushButtonIcon( QIcon(":/misc/delete.png"), FTControls::IconBig );

    saveButton = new FTPushButton( tr("Save map") );
    cancelButton = new FTPushButton( tr("Cancel") );
    loadCalibrationButton = new FTPushButton( tr("Load calibration...") );
    calibrationButton = new FTPushButton( tr("Manual calibration...") );

    calibratedLabel = new FTLabel("xx");

    connect( saveButton, SIGNAL(clicked()),
            this, SLOT(saveEditMap()));
    connect( cancelButton, SIGNAL(clicked()),
            this, SLOT(cancelEditMap()) );
    connect( deleteMapButton, SIGNAL(clicked()),
            this, SLOT(deleteMapUser()));
    connect( loadCalibrationButton, SIGNAL(clicked()),
             this, SLOT(loadCalibration()) );
    connect( calibrationButton, SIGNAL(clicked()),
             this, SLOT(openManualCalibration()) );

    calibrator = new MapCalibrator();
    connect( calibrator, SIGNAL(mapCalibrated(QList<map_cal_point>)),
            this, SLOT(saveManualCalibration(QList<map_cal_point>)) );
    connect( calibrator, SIGNAL(abortCalibration()),
            this, SLOT(closeManualCalibration()));

    editTabs->addWidget( calibrator );

}

void MapsManager::buildMainPage()
{
    pages = new QStackedWidget();
    mainPage = new QWidget();
    pages->addWidget( mainPage );

    mapsList = new FTWidgetList();
    connect( mapsList, SIGNAL(orderChanged(QList<int>)),
            this, SLOT(sortMaps(QList<int>)));
    connect( mapsList, SIGNAL(itemDeleted(int)),
            this, SLOT(deleteMap(int)));
    connect( mapsList, SIGNAL(selectedItem(int)),
            this, SLOT(editMap(int)));

    loadButton = new FTPushButton( tr("Load...") );
    connect( loadButton, SIGNAL(clicked()),
             this, SLOT(importNewMap()));

    showButton = new FTPushButton( tr("Show maps") );
    showButton->setCheckable(true);
    showButton->setChecked( configuration->showMapsOnStartup() );
    connect( showButton, SIGNAL(clicked(bool)),
             this, SIGNAL(enableMaps(bool)) );     

    numMaps = new FTLabel();
    numMaps->setAlignment( Qt::AlignHCenter );
}

void MapsManager::updateCalibrateLabel()
{
    if ( selectedMap == NULL )
        return;

    if ( selectedMap->isCalibrated() || (calibrationPoints.count() >= 3) )
        calibratedLabel->setText( tr("Map is calibrated.") );
    else
        calibratedLabel->setText( tr("Map is NOT calbrated!") );
}

void MapsManager::deleteMap(int i)
{
    selectedMap = bind_maps.value( i, NULL );
    deleteMapUser();
}

void MapsManager::editMap(int id)
{
    Map* map = bind_maps.value( id, NULL );
    if ( map != NULL )
        editMap( map );
}

void MapsManager::editMap(Map * map )
{
    selectedMap = map;
    titleEdit->setText( map->title() );
    enableMapButton->setChecked( map->isEnabledMap() );
    selectedMap->calibrationPoints( calibrationPoints );
    updateCalibrateLabel();
    pages->setCurrentWidget( editTabs );
}

void MapsManager::cancelEditMap()
{
    selectedMap = NULL;
    pages->setCurrentWidget( mainPage );
}

void MapsManager::saveEditMap()
{
    if ( selectedMap != NULL )
    {
        selectedMap->setTitle( titleEdit->text() );
        selectedMap->enableMap( enableMapButton->isChecked() );
        selectedMap->setCalibrationPoints( calibrationPoints );
        saveConfigFile();
        int i = bind_maps.key( selectedMap );
        if ( i > -1 )
            map_labels.value(i)->setText( titleEdit->text() );
        invalidateBlocks();
    }
    cancelEditMap();
}

void MapsManager::loadCalibration()
{
    QString tit = tr("Load calibration");
    if ( selectedMap->isCalibrated() )
    {
        if ( !FTMessageBox::Yesno(tit,
                                  tr("Current calibration will be lost. Continue?")))
            return;
    }
    QStringList filters;
    filters << "*.map";
    filters << "*.imp";
    filters << "*.cal";
    QString filename = FileBrowser::browseFilename(FileBrowser::loadMode,
                                                   tr("Load map calibration..."),
                                                   configuration->defaultPath(),
                                                   filters);

    if ( filename != "" )
    {
        QList<map_cal_point> points;
        if ( Map::loadCalibration( filename, points ) )
        {
            FTMessageBox::Info( tit, tr("Calibration loaded!") );
            calibrationPoints = points;
        }
        else
            FTMessageBox::Error( tit, tr("Unable to load calibration. 3 points minimum required!") );
    }
    updateCalibrateLabel();
}

void MapsManager::openManualCalibration()
{
    if ( selectedMap != NULL )
    {
        calibrator->setupCalibrator( selectedMap, calibrationPoints );
        editTabs->setCurrentWidget( calibrator );
    }
}

void MapsManager::closeManualCalibration()
{
    editTabs->setCurrentWidget( editPage );
    updateCalibrateLabel();
}

void MapsManager::saveManualCalibration(QList<map_cal_point> points)
{
    calibrationPoints = points;
    editTabs->setCurrentWidget( editPage );
    updateCalibrateLabel();
}

void MapsManager::deleteMapUser()
{
    if ( selectedMap == NULL )
        return;

    if ( FTMessageBox::Yesno( tr("Delete map"), tr("Really delete map from storage?") ) )
    {
        selectedMap->clearCache();
        maps.takeAt( maps.indexOf( selectedMap ) );
        int i = bind_maps.key( selectedMap, -1 );
        if ( i > -1 )
        {
            map_labels.take( i );
            mapsList->removeWidget(i);
            delete bind_maps.take( i );
        }
        selectedMap = NULL;
        saveConfigFile();
        updateNumMaps();
        cancelEditMap();
        invalidateBlocks();
    }
}

void MapsManager::sortMaps(QList<int> new_order)
{
    maps.clear();
    foreach (int id, new_order)
    {
        if ( bind_maps.contains( id ) )
            maps.append( bind_maps.value( id ) );
    }
    saveConfigFile();
    invalidateBlocks();
}

void MapsManager::importNewMap()
{
    QStringList filters;
    filters << "*.jpg";
    filters << "*.png";
    QString filename = FileBrowser::browseFilename(FileBrowser::loadMode,
                                                   tr("Load map..."),
                                                   configuration->defaultPath(),
                                                   filters);

    if ( filename != "" )
    {
        Map* map = new Map( filename );
        if ( map->isValidMap() )
        {
            appendMap(map);
            FTMessageBox::Info( tr("Map loaded!"),
                               map->isCalibrated() ? tr("Map loaded and calibrated!") :
                                                     tr("Map loaded but not calibrated!") );
            saveConfigFile();
            editMap(map);
        }
        else
            delete map;
    }
}

void MapsManager::appendMap(Map * map)
{
    maps.append( map );
    int id = maps.indexOf(map);

    bind_maps.insert( id, map );
    map_labels.insert(id,  new FTLabel(map->title()) );

    mapsList->appendWidget( id, map_labels.value( id ) );
    updateNumMaps();
    invalidateBlocks();
}

bool MapsManager::mapsEnabled() const
{
    return showButton->isChecked();
}

void MapsManager::toggleEnableMap()
{
    requestEnableMap( !mapsEnabled() );
}

void MapsManager::requestEnableMap(bool b)
{
    showButton->setChecked(b);
    emit enableMaps(b);
}

void MapsManager::paintMap(QPainter& painter,
                              const QTransform& wgs84_to_table,
                              const QRectF& visible_rect,
                              double density_lat,
                              double density_lon) const
{
    static int cnt = 0;
    // The entire visible rectangle is split in blocks. Each block is a QPixmap (for speed reasons)
    // and is a square. Its side is calculated so that the smaller side of the visible rect will contain
    // at most three blocks.
    double block_size = qMin( visible_rect.width(), visible_rect.height() ) / 2;
    // Store as QSizeF for later reuse
    QSizeF bs = QSizeF( block_size, block_size );

    // Now, we will iterate on all the blocks we need to draw. Since we want to cache the
    // blocks to avoid redrawing them each time, we divide the entire world (!) in blocks
    // given the block size. qFloor is needed to avoid int mishandling for x < 0, which
    // rounds to the "next" integer instead of the previous! (es: -1,05 becomes -1 and NOT -2)
    int first_block_lat = qFloor(visible_rect.top()    / block_size);
    int last_block_lat  = qFloor(visible_rect.bottom() / block_size);
    int first_block_lon = qFloor(visible_rect.left()   / block_size);
    int last_block_lon  = qFloor(visible_rect.right()  / block_size);

    // Lets iterate all the blocks:
    for ( int lat = first_block_lat; lat <= last_block_lat; lat += 1 )
    {
        for ( int lon = first_block_lon; lon <= last_block_lon; lon += 1 )
        {
            // For each block we need to calculate a few things.

            // First: the rectangle covered by the block (in lat/lon):
            QRectF block_rect =  QRectF( QPointF( lon * block_size, lat * block_size ), bs).normalized();

            // Second: the actual screen coordinates for the block:
            QRect screen_rect = wgs84_to_table.mapRect( block_rect ).toRect().normalized();

            // Third: the block key for cache lookup:
            QString block_key = QString("%1 - %2 - %3").arg( lat )
                                                       .arg( lon )
                                                       .arg( block_size, 0, 'f', 5);

            // Let get the block...
            QPixmap block;
            // Is the block already cached?
            if ( !QPixmapCache::find( block_key, block ) )
            {   // No... we shall build it...
                createBlock( block, block_rect, density_lat, density_lon );
                // ... and store in cache.
                QPixmapCache::insert( block_key, block );
                cnt++;
            }
            // Since a block will not be constructed if there is no maps in it,
            // lets avoid drawing null blocks
            if ( !block.isNull() )
            {
                painter.drawPixmap( screen_rect.topLeft(), block );
            }
        }
    }
}

void MapsManager::createBlock(QPixmap& block,
                              const QRectF & visible_rect,
                              double density_lat, double density_lon) const
{
    // What we shall do here is draw a block.
    // To do this, we need to find out which maps intersects the block area,
    // then we will draw those maps on the block


    // We use polygons, so we can subtract and add them easily.
    QPolygonF visible_area( visible_rect );
    // These polygons will hold, each, the visible area of a map
    QList<QPolygonF> map_areas;

    // First, we identify all the maps which intersects the requested area:
    QList<const Map*> local_maps;
    int nMaps = maps.count();
    for ( int i = 0; i < nMaps; i++ ) // int i = nMaps-1; i > -1; i--) // int i = 0; i < nMaps; i++ )
    {
        // Dont keep iterating once the visible area has all been assigned!
        if ( visible_area.isEmpty() )
            break;
        const Map* map = maps.at(i);
       // Skip disabled and uncalibrated maps
        if ( map->isEnabledMap() && map->isCalibrated() )
        {
            // Create a polygon wich intersects both the map and the area (map is derived from QRectF)
            QPolygonF map_area = visible_area.intersected( *map );
            // If its not empty...
            if ( !map_area.isEmpty() )
            {
                // We need to paint this map
                local_maps.append( map );
                // And we add the intesected area to the areas to be drawn
                map_areas.append( map_area );
                // Then we reduce the visible area left
                visible_area = visible_area.subtracted( map_area );
            }
        }
    }
    nMaps = local_maps.count();
    // Quickie, just return in case there are no maps to be drawn
    if ( nMaps <= 0 )
        return;

    // Now to the real drawing... First of all, here we allocate the pixmap:
    block = QPixmap(visible_rect.width()  * density_lon,
                    visible_rect.height() * density_lat);

    // clear it
    block.fill( QColor("black") );

    QTransform wgs84_to_block;
    wgs84_to_block.scale( density_lon, -density_lat );
    wgs84_to_block.translate( -visible_rect.left(), -visible_rect.bottom() );

    // Since we can paint on a pixmap, we create a painter for it:
    QPainter painter;
    // Explicitly start...
    if ( !painter.begin( &block ) )
        return;

    // Now we iterate on all the intersecting maps
    for ( int i = nMaps-1; i > -1; i-- ) //int i = 0; i < nMaps; i++ )
    {   // Get the map...
        const Map* map = local_maps.at(i);
        // Paint the map on the block
        map->paint( painter, wgs84_to_block, map_areas.at(i).boundingRect(), density_lat, density_lon );
    }
    // End paint...
    painter.end();
}

void MapsManager::updateNumMaps()
{
    if ( maps.count() > 0 )
        numMaps->setText( tr("%1 maps loaded!").arg( maps.count() ) );
    else
        numMaps->setText( tr("no maps loaded!") );
}
